const WebRtcStreamer = (function () {
  class WebRtcStreamer {
    /**
     * Interface with WebRTC-streamer API
     * @constructor
     * @param {string} videoElement - id of the video element tag
     * @param {string} srvurl -  url of webrtc-streamer (default is current location)
     */
    constructor(videoElement, srvurl) {
      if (typeof videoElement === 'string') {
        this.videoElement = document.getElementById(videoElement)
      } else {
        this.videoElement = videoElement
      }
      this.srvurl = srvurl || location.protocol + '//' + window.location.hostname + ':' + window.location.port
      this.pc = null

      this.mediaConstraints = { offerToReceiveAudio: true, offerToReceiveVideo: true }

      this.iceServers = null
      this.earlyCandidates = []
    }
    _handleHttpErrors(response) {
      if (!response.ok) {
        throw Error(response.statusText)
      }
      return response
    }
    /**
     * Connect a WebRTC Stream to videoElement
     * @param {string} videourl - id of WebRTC video stream
     * @param {string} audiourl - id of WebRTC audio stream
     * @param {string} options  -  options of WebRTC call
     * @param {string} stream   -  local stream to send
     * @param {string} prefmime -  prefered mime
     */
    connect(videourl, audiourl, options, localstream, prefmime) {
      this.disconnect()

      // getIceServers is not already received
      if (!this.iceServers) {
        console.log('Get IceServers')

        const { browserName, browserVersion, osName } = this.getDeviceInfo()

        fetch(this.srvurl + `/api/getIceServers?browserName=${browserName}&browserVersion=${browserVersion}&osName=${osName}`, )
          .then(this._handleHttpErrors)
          .then((response) => response.json())
          .then((response) => this.onReceiveGetIceServers(response, videourl, audiourl, options, localstream, prefmime))
          .catch((error) => this.onError('getIceServers ' + error))
      } else {
        this.onReceiveGetIceServers(this.iceServers, videourl, audiourl, options, localstream, prefmime)
      }
    }
    /**
     * Disconnect a WebRTC Stream and clear videoElement source
     */
    disconnect() {
      if (this.videoElement?.srcObject) {
        this.videoElement.srcObject.getTracks().forEach((track) => {
          track.stop()
          this.videoElement.srcObject.removeTrack(track)
        })
      }
      if (this.pc) {
        fetch(this.srvurl + '/api/hangup?peerid=' + this.pc.peerid)
          .then(this._handleHttpErrors)
          .catch((error) => this.onError('hangup ' + error))

        try {
          this.pc.close()
        } catch (e) {
          console.log('Failure close peer connection:' + e)
        }
        this.pc = null
      }
    }
    /**
     * GetIceServers callback
     */
    onReceiveGetIceServers(iceServers, videourl, audiourl, options, stream, prefmime) {
      console.log('iceServers: ', iceServers)
      this.iceServers = iceServers
      this.pcConfig = iceServers || { iceServers: [] }
      try {
        this.createPeerConnection()

        var callurl = this.srvurl + '/api/call?peerid=' + this.pc.peerid + '&url=' + encodeURIComponent(videourl)
        if (audiourl) {
          callurl += '&audiourl=' + encodeURIComponent(audiourl)
        }
        if (options) {
          callurl += '&options=' + encodeURIComponent(options)
        }

        if (stream) {
          this.pc.addStream(stream)
        }

        // clear early candidates
        this.earlyCandidates.length = 0

        // create Offer
        this.pc.createOffer(this.mediaConstraints).then(
          (sessionDescription) => {
            console.log('Create offer:' + JSON.stringify(sessionDescription))

            console.log(
              `video codecs:${Array.from(new Set(RTCRtpReceiver.getCapabilities('video')?.codecs?.map((codec) => codec.mimeType)))}`
            )
            console.log(
              `audio codecs:${Array.from(new Set(RTCRtpReceiver.getCapabilities('audio')?.codecs?.map((codec) => codec.mimeType)))}`
            )

            if (prefmime != undefined) {
              //set prefered codec
              const [prefkind] = prefmime.split('/')
              const codecs = RTCRtpReceiver.getCapabilities(prefkind).codecs
              const preferedCodecs = codecs.filter((codec) => codec.mimeType === prefmime)

              console.log(`preferedCodecs:${JSON.stringify(preferedCodecs)}`)
              this.pc
                .getTransceivers()
                .filter((transceiver) => transceiver.receiver.track.kind === prefkind)
                .forEach((tcvr) => {
                  if (tcvr.setCodecPreferences != undefined) {
                    tcvr.setCodecPreferences(preferedCodecs)
                  }
                })
            }

            this.pc.setLocalDescription(sessionDescription).then(
              () => {
                fetch(callurl, {
                  method: 'POST',
                  headers: {
                    'Content-Type': 'application/json',
                  },
                  body: JSON.stringify(sessionDescription),
                })
                  .then(this._handleHttpErrors)
                  .then((response) => response.json())
                  .catch((error) => this.onError('call ' + error))
                  .then((response) => this.onReceiveCall(response))
                  .catch((error) => this.onError('call ' + error))
              },
              (error) => {
                console.log('setLocalDescription error:' + JSON.stringify(error))
              }
            )
          },
          (error) => {
            alert('Create offer error:' + JSON.stringify(error))
          }
        )
      } catch (e) {
        this.disconnect()
        alert('connect error: ' + e)
      }
    }
    getIceCandidate() {
      fetch(this.srvurl + '/api/getIceCandidate?peerid=' + this.pc.peerid)
        .then(this._handleHttpErrors)
        .then((response) => response.json())
        .then((response) => this.onReceiveCandidate(response))
        .catch((error) => this.onError('getIceCandidate ' + error))
    }
    /**
     * create RTCPeerConnection
     */
    createPeerConnection() {
      console.log('createPeerConnection  config: ' + JSON.stringify(this.pcConfig))
      this.pc = new RTCPeerConnection(this.pcConfig)
      var pc = this.pc
      pc.peerid = Math.random()

      pc.onicecandidate = (evt) => this.onIceCandidate(evt)
      pc.onaddstream = (evt) => this.onAddStream(evt)
      pc.oniceconnectionstatechange = (evt) => {
        if (this.videoElement) {
          if (pc.iceConnectionState === 'connected') {
            console.log('\x1b[32m%s\x1b[0m', 'oniceconnectionstatechange  state: ' + pc.iceConnectionState)
            this.videoElement.style.opacity = '1.0'
          } else if (pc.iceConnectionState === 'disconnected') {
            console.log('\x1b[31m%s\x1b[0m', 'oniceconnectionstatechange  state: ' + pc.iceConnectionState)
            this.videoElement.style.opacity = '0.25'
          } else if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'closed') {
            this.videoElement.style.opacity = '0.5'
          } else if (pc.iceConnectionState === 'new') {
            this.getIceCandidate()
          }
        }
      }
      pc.ondatachannel = function (evt) {
        console.log('remote datachannel created:' + JSON.stringify(evt))

        evt.channel.onopen = function () {
          console.log('remote datachannel open')
          this.send('remote channel openned')
        }
        evt.channel.onmessage = function (event) {
          console.log('remote datachannel recv:' + JSON.stringify(event.data))
        }
      }

      try {
        var dataChannel = pc.createDataChannel('ClientDataChannel')
        dataChannel.onopen = function () {
          console.log('local datachannel open')
          this.send('local channel openned')
        }
        dataChannel.onmessage = function (evt) {
          console.log('local datachannel recv:' + JSON.stringify(evt.data))
        }
      } catch (e) {
        console.log('Cannor create datachannel error: ' + e)
      }

      console.log('Created RTCPeerConnnection with config: ' + JSON.stringify(this.pcConfig))
      return pc
    }
    /**
     * RTCPeerConnection IceCandidate callback
     */
    onIceCandidate(event) {
      if (event.candidate) {
        if (this.pc.currentRemoteDescription) {
          this.addIceCandidate(this.pc.peerid, event.candidate)
        } else {
          this.earlyCandidates.push(event.candidate)
        }
      } else {
        console.log('End of candidates.')
      }
    }
    addIceCandidate(peerid, candidate) {
      fetch(this.srvurl + '/api/addIceCandidate?peerid=' + peerid, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(candidate),
      })
        .then(this._handleHttpErrors)
        .then((response) => response.json())
        .then((response) => {
          console.log('addIceCandidate ok:' + response)
        })
        .catch((error) => this.onError('addIceCandidate ' + error))
    }
    /**
     * RTCPeerConnection AddTrack callback
     */
    onAddStream(event) {
      console.log('Remote track added:' + JSON.stringify(event))

      this.videoElement.srcObject = event.stream
      var promise = this.videoElement.play()
      if (promise !== undefined) {
        promise.catch((error) => {
          console.warn('error:' + error)
          this.videoElement.setAttribute('controls', true)
        })
      }
    }
    /**
     * AJAX /call callback
     */
    onReceiveCall(dataJson) {
      console.log('offer: ' + JSON.stringify(dataJson))
      var descr = new RTCSessionDescription(dataJson)
      this.pc.setRemoteDescription(descr).then(
        () => {
          console.log('setRemoteDescription ok')
          while (this.earlyCandidates.length) {
            var candidate = this.earlyCandidates.shift()
            this.addIceCandidate(this.pc.peerid, candidate)
          }

          this.getIceCandidate()
        },
        (error) => {
          console.log('setRemoteDescription error:' + JSON.stringify(error))
        }
      )
    }
    /**
     * AJAX /getIceCandidate callback
     */
    onReceiveCandidate(dataJson) {
      if (dataJson) {
        for (var i = 0; i < dataJson.length; i++) {
          var candidate = new RTCIceCandidate(dataJson[i])

          console.log('Adding ICE candidate :' + JSON.stringify(candidate))
          this.pc.addIceCandidate(candidate).then(
            () => {
              console.log('addIceCandidate OK')
            },
            (error) => {
              console.log('addIceCandidate error:' + JSON.stringify(error))
            }
          )
        }
        this.pc.addIceCandidate()
      }
    }
    /**
     * AJAX callback for Error
     */
    onError(status) {
      console.log('onError:' + status)
    }

    /**
     * Get device information
     */
    getDeviceInfo() {
      const userAgent = navigator.userAgent;

      // Extract browser model and version
      const browserMatch = userAgent.match(/(Firefox|MSIE|Trident|Edge|Chrome|Safari|Opera)[\/\s](\d+\.\d+)/);
      const browserName = browserMatch ? browserMatch[1] : 'Unknown';
      const browserVersion = browserMatch ? browserMatch[2] : 'Unknown';

      // Extract the operating system model
      const osMatch = userAgent.match(/(Windows NT|Mac OS X|Android|iOS|Linux|CrOS) ([\d\.]+)/);
      const osName = osMatch ? osMatch[1] : 'Unknown';
      const osVersion = osMatch ? osMatch[2] : 'Unknown';

      // Device information object
      const deviceInfo = {
        browserInfo: userAgent,
        browserName: browserName,
        browserVersion: browserVersion,
        osName: osName,
        osVersion: osVersion,
        isMobile: /Mobi|Android/i.test(userAgent),
        platform: navigator.platform,
        screenResolution: `${screen.width}x${screen.height}`,
        language: navigator.language || navigator.userLanguage,
        isOnline: navigator.onLine,
        deviceMemory: navigator.deviceMemory ? `${navigator.deviceMemory} GB` : 'Unknown',
        isTouchDevice: 'ontouchstart' in window || navigator.maxTouchPoints > 0,
      };

      return deviceInfo;
    }
  }

  return WebRtcStreamer
})()

if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
  window.WebRtcStreamer = WebRtcStreamer
}
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  module.exports = WebRtcStreamer
}
